vlwkaos' digital garden

Git commit message linting - commtilint

개요

  • 모노레포 구조상 Changelog 관리가 필요하다.
  • Changelog 자동화는 일반적으로 conventional commit을 사용한다.
  • conventional commit을 강제하거나 자동화할 도구가 있을까?
  • commitizen과 commitlint에 대해 알아보자

commitlint

  • commitlint는 커밋 메세지에 대한 linting을 제공한다.
  • 여러가지 linting 규칙을 설정할 수 있다.

설치

다음 커맨드를 입력하여  commitlint를 설치한다.

@commitlint/config-conventional 패키지는 conventional commit의 rule을 적용시킨 프리셋 설정 이다.

# npm
npm install -D @commitlint/cli @commitlint/config-conventional
# yarn
yarn add -D @commitlint/cli @commitlint/config-conventional

그리고 프로젝트 루트에 commitlint.config.js 를 생성하고 아래를 입력한다.

module.exports = {
  extends: ['@commitlint/config-conventional'],
}

설치가 됐으면 cli에서 간단하게 테스트 해볼 수 있다.

Lint from stdin

echo 'foo: bar' | commitlint
⧗   input: foo: bar
✖   type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]

✖   found 1 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
  • 위가 실행되지 않는 경우 yarn commitlint 등으로 실행해볼 수 있다.

설정 커스터마이징

https://commitlint.js.org/#/reference-configuration

파싱 옵션

규칙은 메세지를 파싱한 뒤에 메세지에 어떤 요소가 파싱되었는지에 따라 조건을 통해 pass/fail로 결정난다. 

때문에 파싱을 어떻게 할건지에 대해서도 설정이 가능하다.

커밋 메세지의 가장 윗줄에 feat(core): (KE-1234) subject  와 같은 형태로 메세지를 파싱하도록 적용해보자.

module.exports = {
  extends: ['@commitlint/config-conventional'],
  parserPreset: {
    parserOpts: {
      headerPattern: /^(\w*)(?:\((\w*)\))?:\s?(?:\((\w+-\d+)\)\s)?(.*)$/,
      headerCorrespondence: ['type', 'scope', 'ticket', 'subject'],
    },
  }
}
  • headerPattern  설정 값에 규칙을 정규식으로 표현할 수 있다.

  • headerCorrespondence: 에는 각 캡쳐그룹이 헤더의 어떤 부분을 인식할 것인지 지정할 수 있다.(토큰)

설명으로 풀어보자

  • 처음 ^(\w*) 는 word를 캡쳐하고 이는 type에 대응된다. 예) feat, docs, fix, ...
  • 다음 (?:\((\w*)\))?
    • 가장 바깥 괄호는 ?:기호를 사용하므로 캡쳐되지 않는다. 용도는 마지막에 ?로 옵셔널하게 입력되는 값인지에 대해 그루핑하기 위해서이다.
    • 이후 escaped 괄호 \( * \) 안의 (\w*)는 word를 캡쳐하고 scope에 대응된다. 예) (core), (table)
  • 다음 :\s 세미콜론 다음 공백이 들어오거나 없는 형태
  • 다음 (?:\((\w+-\d+)\)\s)? 
    • 마찬가지로 바깥 괄호는 선택적으로 인식하기 위해 그룹하는 용도이다 
    •  이후 캡쳐되는 부분은 위 scope와 마찬마지로 (\w+-\d) 부분이다 이는 ticket에 대응된다. 예) (TICKET-1234)
  • 마지막 (.*)$ 는 아무 내용이 올 수 있다. subject에 대응된다. 예) 제목입니다

규칙

자세한 사항은 여기를 참조: https://commitlint.js.org/#/reference-rules

각 규칙은 다음 3가지 값을 갖는 배열로 설정되어야한다. (혹은 3가지 값을 갖는 배열을 반환하는 함수여도 된다)

  • Level [0..2]0 해제, 1 경고(알림), 2 에러(커밋 불가) 
  • Applicable always|never: never로 설정할 경우 규칙을 반대로 적용한다.
  • Value: 룰에서 사용할 기타 값

위의 파싱 설정을 한 경우 규칙대로 캡쳐된 값을 룰셋에 넘겨줄 수 있다.

ticket이라는 토큰에 대해 commitlint는 알지 못하기 때문에 이를 강제하거나 경고하기 위해서는 별도의 rule을 추가해줘야한다.

커밋 메세지에 ticket이 없을때를 대응하는 커스텀 룰을 하나 만들어보자.

module.exports = {
    //...
  plugins: [
    {
      rules: {
        'ticket-empty': parsed => {
          cosnt {ticket} = parsed; // 이런식으로 위에서 지정한 토큰에 대응되는 부분을 가져온다
          if (ticket === null) {
            return [
              false, // false일 경우 linting 통과하지 못한 것이다.
              '이슈 번호가 없습니다!' // 적절한 경고 문구를 전달한다.
            ]
          }
          return [true, ''] // 조건 통과의 경우
        }
      }
    }   
  ],
  rules: {
    'ticket-empty': [1, 'always'] // 위에서 만든 룰 적용
  }
}
Git commit message linting - commtilint